@file:OptIn(ExperimentalTime::class)

package com.gitee.wsl.time.format

import com.gitee.wsl.time.create
import com.gitee.wsl.time.millisecond
import com.gitee.wsl.time.toDisplayString
import kotlinx.datetime.FixedOffsetTimeZone
import kotlin.time.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.UtcOffset
import kotlinx.datetime.asTimeZone
import kotlinx.datetime.isoDayNumber
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import kotlin.time.ExperimentalTime

/**
 * Formats:
 *
 * Symbol   Meaning                     Examples
 * ------   -------                     --------
 * y        year                        2023; 23
 * M        month                       7; 07
 * d        day of month                9; 09; 31
 *
 * H        hour (0~23)                 0; 00; 18
 * h        hour (1~12)                 6; 06; 10
 * m        minute                      4; 04; 58
 * s        second                      5; 05; 59
 * l        millisecond                 12; 120; 123
 *
 * aa       am/pm                       am; pm
 * AA       AM/PM                       AM; PM
 * e        Day of week. 0=Sun, 6=Sat.  1
 * E        Day of week.                Mon
 *
 * Z        Time zone offset or 'Z'     Z; +08:00
 * z        Time zone offset            +00:00; +08:00
 *
 * '        Literal         '
 */
class DateTimeFormat(val pattern: String) {

    var weekDayNames = WEEKDAY_NAMES
        set(value) {
            if (value.size != 7) throw IllegalArgumentException("`weekDayNames` must be a list of 7 string elements")
            field = value.toList()
        }

    enum class FormatTokenType(val hasLengthLimit: Boolean = true, val allowedLengths: List<Int> = listOf()) {
        Literial(hasLengthLimit = false),
        Year(allowedLengths = listOf(2, 4)),
        Month(allowedLengths = listOf(1, 2)),
        DayOfMonth(allowedLengths = listOf(1, 2)),
        Hour_0_23(allowedLengths = listOf(1, 2)),
        Hour_1_12(allowedLengths = listOf(1, 2)),
        Minute(allowedLengths = listOf(1, 2)),
        Second(allowedLengths = listOf(1, 2)),
        Millisecond(allowedLengths = listOf(1, 2, 3)),
        ampm(allowedLengths = listOf(1,2)),
        AMPM(allowedLengths = listOf(1,2)),
        DayOfWeekNumber(allowedLengths = listOf(1)),
        DayOfWeek(allowedLengths = listOf(1,3,4)),
        TimeZoneOffsetOrZ(allowedLengths = listOf(1)),
        TimeZoneOffset(allowedLengths = listOf(1)),
    }

    data class FormatToken(val type: FormatTokenType, val length: Int, val literal: String? = null)

    val tokens: List<FormatToken>

    init {
        val tokens = mutableListOf<FormatToken>()
        var isConsumingLiteral = false
        var literal = StringBuilder()
        var type: FormatTokenType? = null
        var prevType: FormatTokenType? = null
        var length = 0

        fun addPrevToken() {
            prevType?.let { prevType ->
                if (prevType.hasLengthLimit && !prevType.allowedLengths.contains(length)) {
                    throw IllegalArgumentException("Invalid length of the token type $prevType. Allowed length: ${prevType.allowedLengths}; actual length: $length")
                }
                val token = if (prevType == FormatTokenType.Literial) {
                    FormatToken(type = prevType, length = literal.length, literal = literal.toString())
                } else {
                    FormatToken(type = prevType, length = length)
                }
                tokens += token
            }
        }

        pattern.forEach {
            var ignoreCurrentChar = false

            if (isConsumingLiteral && it != '\'') {
                type = FormatTokenType.Literial
            } else if (it == '\'') {
                type = FormatTokenType.Literial
                isConsumingLiteral = !isConsumingLiteral
                ignoreCurrentChar = true
            } else {
                type = when (it) {
                    'y' -> FormatTokenType.Year
                    'M' -> FormatTokenType.Month
                    'd' -> FormatTokenType.DayOfMonth
                    'H' -> FormatTokenType.Hour_0_23
                    'h' -> FormatTokenType.Hour_1_12
                    'm' -> FormatTokenType.Minute
                    's' -> FormatTokenType.Second
                    'l' -> FormatTokenType.Millisecond
                    'a' -> FormatTokenType.ampm
                    'A' -> FormatTokenType.AMPM
                    'e' -> FormatTokenType.DayOfWeekNumber
                    'E' -> FormatTokenType.DayOfWeek
                    'Z' -> FormatTokenType.TimeZoneOffsetOrZ
                    'z' -> FormatTokenType.TimeZoneOffset
                    else -> FormatTokenType.Literial
                }
            }

            if (type == prevType) {
                ++length
            } else {
                addPrevToken()
                prevType = type
                literal.clear()
                length = 1
            }
            if (!ignoreCurrentChar && type == FormatTokenType.Literial) {
                literal.append(it)
            }
        }
        prevType = type
        addPrevToken()

        this.tokens = tokens.toList()
    }

    fun format(datetime: LocalDateTime,offset: UtcOffset?=null): String {
        val localDate = datetime.date

        val s = StringBuilder()
        tokens.forEach { it ->
            when (it.type) {
                FormatTokenType.Literial -> s.append(it.literal)
                FormatTokenType.Year -> s.append(localDate.year.toString().substring(if (it.length == 2) 2 else 0))
                FormatTokenType.Month -> s.append(localDate.monthNumber.toString().padStart(it.length, '0'))
                FormatTokenType.DayOfMonth -> s.append(localDate.dayOfMonth.toString().padStart(it.length, '0'))
                FormatTokenType.Hour_0_23 -> s.append(
                    datetime.hour.toString().padStart(it.length, '0'))
                FormatTokenType.Hour_1_12 -> {
                    val halfHour = datetime.hour % 12
                    s.append((if (halfHour == 0) 12 else halfHour).toString().padStart(it.length, '0'))
                }
                FormatTokenType.Minute -> s.append(
                    datetime.minute.toString().padStart(it.length, '0'))
                FormatTokenType.Second -> s.append(
                    datetime.second.toString().padStart(it.length, '0'))
                FormatTokenType.Millisecond -> s.append(
                    datetime.millisecond.toString().padStart(3, '0').trimEnd('0').padEnd(it.length, '0'))
                FormatTokenType.ampm -> s.append(if (datetime.hour < 12) "am" else "pm")
                FormatTokenType.AMPM -> s.append(if (datetime.hour < 12) "AM" else "PM")
                FormatTokenType.DayOfWeekNumber -> s.append(localDate.dayOfWeek)
                FormatTokenType.DayOfWeek -> s.append(weekDayNames[localDate.dayOfWeek.isoDayNumber])
                FormatTokenType.TimeZoneOffsetOrZ -> offset?.let{ s.append(offset.toDisplayString(true))}
                FormatTokenType.TimeZoneOffset -> s.append(offset?.toDisplayString(false)?:"+00:00")
            }
        }
        return s.toString()
    }

    fun format(localDate: LocalDate): String {
        val s = StringBuilder()
        tokens.forEach {
            when (it.type) {
                FormatTokenType.Literial -> s.append(it.literal)
                FormatTokenType.Year -> s.append(localDate.year.toString().substring(if (it.length == 2) 2 else 0))
                FormatTokenType.Month -> s.append(localDate.monthNumber.toString().padStart(it.length, '0'))
                FormatTokenType.DayOfMonth -> s.append(localDate.dayOfMonth.toString().padStart(it.length, '0'))
                FormatTokenType.DayOfWeekNumber -> s.append(localDate.dayOfWeek)
                FormatTokenType.DayOfWeek -> s.append(weekDayNames[localDate.dayOfWeek.isoDayNumber])
                /*FormatTokenType.TimeZoneOffsetOrZ -> when (datetime) {
                    is KZonedInstant -> s.append(datetime.zoneOffset.toDisplayString())
                    is KInstant -> s.append("Z")
                    else -> throw UnsupportedOperationException("${datetime::class.simpleName} has no timezone support")
                }
                FormatTokenType.TimeZoneOffset -> when (datetime) {
                    is Instant -> s.append(datetime.zoneOffset.toDisplayString(isDisplayZ = false))
                    is Instant -> s.append("+00:00")
                    else -> throw UnsupportedOperationException("${datetime::class.simpleName} has no timezone support")
                }*/
                else ->{}
            }
        }
        return s.toString()
    }

    @OptIn(ExperimentalTime::class)
    fun format(time: LocalTime): String {
        val s = StringBuilder()
        tokens.forEach {
            when (it.type) {
                FormatTokenType.Literial -> s.append(it.literal)
                FormatTokenType.Hour_0_23 -> s.append(
                    time.hour.toString().padStart(it.length, '0'))
                FormatTokenType.Hour_1_12 -> {
                    val halfHour = time.hour % 12
                    s.append((if (halfHour == 0) 12 else halfHour).toString().padStart(it.length, '0'))
                }
                FormatTokenType.Minute -> s.append(
                    time.minute.toString().padStart(it.length, '0'))
                FormatTokenType.Second -> s.append(
                    time.second.toString().padStart(it.length, '0'))
                FormatTokenType.Millisecond -> s.append(
                    time.millisecond.toString().padStart(3, '0').trimEnd('0').padEnd(it.length, '0'))
                FormatTokenType.ampm -> s.append(if (time.hour < 12) "am" else "pm")
                FormatTokenType.AMPM -> s.append(if (time.hour < 12) "AM" else "PM")
                else ->{}
            }
        }
        return s.toString()
    }


    protected fun validateForFullValidParser() {
        val tokenTypesList = tokens.map { it.type }
            .filter { it != FormatTokenType.Literial }
        val tokenTypesSet = tokenTypesList.toSet()

        if (tokenTypesSet.size != tokenTypesList.size) {
            throw IllegalArgumentException("A parser cannot have repeated fields")
        }

        if (!tokenTypesSet.containsAll(PARSER_COMPULSORY_INPUT_TYPES)) {
            throw IllegalArgumentException("Missing compulsory fields in the pattern")
        }

        if (tokenTypesSet.contains(FormatTokenType.Hour_0_23)) {
            if (tokenTypesSet.contains(FormatTokenType.Hour_1_12) || tokenTypesSet.contains(FormatTokenType.AMPM) || tokenTypesSet.contains(FormatTokenType.ampm)) {
                throw IllegalArgumentException("Cannot contain contradicting fields in the pattern")
            }
        } else if (tokenTypesSet.contains(FormatTokenType.Hour_1_12)) {
            if (!tokenTypesSet.contains(FormatTokenType.AMPM) && !tokenTypesSet.contains(FormatTokenType.ampm)) {
                throw IllegalArgumentException("Missing compulsory fields in the pattern")
            }
        } else {
            throw IllegalArgumentException("Missing compulsory fields in the pattern")
        }
    }

    protected fun parseAmPm(input: String): Int {
        return when (input.lowercase()) {
            "am" -> AM
            "pm" -> PM
            else -> throw ParseDateTimeException()
        }
    }

    fun parseToKZonedDateTime(input: String): Instant {
        validateForFullValidParser()

        var year: Int? = null
        var month: Int? = null
        var dayOfMonth: Int? = null
        var hour: Int? = null
        var minute: Int? = null
        var second: Int? = null
        var millisecond: Int? = null
        var amPm: Int? = null // am = 0, pm = 1
        var zoneOffset: FixedOffsetTimeZone? = null

        var startIndex = 0
        tokens.forEach { token ->
            val inputSubstring = input.substring(startIndex, startIndex + token.length)
            var length = token.length
            if (token.type == FormatTokenType.Literial) {
                if (inputSubstring != token.literal!!) {
                    throw ParseDateTimeException()
                }
                startIndex += length
                return@forEach // skip because no field to input
            }

            try {
                when (token.type) {
                    FormatTokenType.Year -> {
                        year = inputSubstring.toInt()
                        if (token.length == 2) {
                            year = year!! + if (year!! >= 70) 1900 else 2000
                        }
                    }

                    FormatTokenType.Month -> month = inputSubstring.toInt()
                    FormatTokenType.DayOfMonth -> dayOfMonth = inputSubstring.toInt()
                    FormatTokenType.Hour_0_23 -> hour = inputSubstring.toInt()
                    FormatTokenType.Hour_1_12 -> hour = inputSubstring.toInt()
                    FormatTokenType.Minute -> minute = inputSubstring.toInt()
                    FormatTokenType.Second -> second = inputSubstring.toInt()
                    FormatTokenType.Millisecond -> millisecond = inputSubstring.toInt()
                    FormatTokenType.ampm -> amPm = parseAmPm(inputSubstring)
                    FormatTokenType.AMPM -> amPm = parseAmPm(inputSubstring)
                    FormatTokenType.TimeZoneOffsetOrZ -> {
                        val longerSubstring = input.substring(startIndex)
                        length = if (longerSubstring.startsWith("Z")) {
                            1
                        } else if (longerSubstring.startsWith("UTC")) {
                            3
                        } else {
                            "+00:00".length
                        }
                        zoneOffset = try {
                            val zoneOffsetString = longerSubstring.substring(0, length)
                            FixedOffsetTimeZone(UtcOffset.parse(zoneOffsetString))
                        } catch (e: IllegalArgumentException) {
                            throw ParseDateTimeException(e.message)
                        }
                    }

                    else -> {}
                }
            } catch (e: NumberFormatException) {
                throw ParseDateTimeException(e.message)
            }
            startIndex += length
        }

        if (!(1..12).contains(month)) {
            throw ParseDateTimeException()
        }

        if (amPm != null) {
            if (!(1..12).contains(hour)) {
                throw ParseDateTimeException()
            }
            if (amPm == AM) {
                if (hour == 12) hour = 0
            } else if (amPm == PM) {
                hour = (hour!! + 12) % 24
            }
        }

        return try {
            val dateTime:LocalDateTime = LocalDateTime.create(
                year = year!!,
                month = month!!,
                day = dayOfMonth!!,
                hour = hour ?: 0,
                minute = minute ?: 0,
                second = second ?: 0,
                millisecond = millisecond ?: 0
            )
            //zoneOffset?.let { dateTime = dateTime.toInstant(TimeZone.UTC).toLocalDateTime(it) }
            //if(zoneOffset!=null) dateTime.toInstant(zoneOffset!!).toLocalDateTime(TimeZone.UTC) else dateTime
             dateTime.toInstant(zoneOffset?: TimeZone.UTC)
        } catch (e: IllegalArgumentException) {
            throw ParseDateTimeException(e.message)
        }
    }

    fun parseToKZonedDateTimeOrNull(input: String): Instant?{
       return try{
           parseToKZonedDateTime(input)
        } catch (e:ParseDateTimeException){
            null
        }
    }

    companion object {
        val ISO8601_DATETIME = DateTimeFormat("yyyy-MM-dd'T'HH:mm:ssZ")
        val FULL = DateTimeFormat("yyyy-MM-dd'T'HH:mm:ss.lllZ")

        // order of the list matters
        val ISO8601_FORMATS = listOf(FULL, ISO8601_DATETIME)
        val IOS_DATE_FORMATS = listOf(FULL, ISO8601_DATETIME, DateTimeFormat("yyyy-MM-dd'T'HH:mmZ"))

        val WEEKDAY_NAMES = listOf("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")

        internal val PARSER_COMPULSORY_INPUT_TYPES = setOf(
            FormatTokenType.Year,
            FormatTokenType.Month,
            FormatTokenType.DayOfMonth,
            FormatTokenType.Minute,
//            FormatTokenType.Second,
            FormatTokenType.TimeZoneOffsetOrZ
        )

        private val AM = 0
        private val PM = 1
    }
}

class ParseDateTimeException(message: String? = null) : RuntimeException(message ?: "Input date/time is invalid or does not match with expected pattern")

/**
 * Format the date time to a String using the Gregorian Calendar.
 */
fun LocalDateTime.format(pattern: String,offset: UtcOffset?=null) = DateTimeFormat(pattern).format(this,offset)

fun LocalDate.format(pattern: String) = DateTimeFormat(pattern).format(this)

fun LocalTime.format(pattern: String) = DateTimeFormat(pattern).format(this)

fun Instant.format(pattern: String,utcOffset: UtcOffset = UtcOffset.ZERO) =this.toLocalDateTime(utcOffset.asTimeZone()).format(pattern,utcOffset)

fun LocalDateTime.format(pattern: DateTimeFormat,offset: UtcOffset?=null) = pattern.format(this,offset)

fun LocalDate.format(pattern: DateTimeFormat) = pattern.format(this)

fun LocalTime.format(pattern: DateTimeFormat) = pattern.format(this)

fun Instant.format(pattern: DateTimeFormat,utcOffset: UtcOffset = UtcOffset.ZERO) = this.toLocalDateTime(utcOffset.asTimeZone()).format(pattern,utcOffset)
